iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
Modern Web

給前端新手的圖文故事系列 第 23

學習基礎 Hook 使用與副作用(Side Effect)

  • 分享至 

  • xImage
  •  

基礎 Hook 操作

Hook 是在 React 16.8 之後加入的一組內建函式,用於在函式(Function)型組件中添加狀態管理和模擬生命週期特性。
Hooks 的引入讓我們能夠在無需轉換為類別(Class)型組件的情況下,更方便地處理狀態和副作用。

useState 學習

useState 是 React 中最基本和最常用的 Hook 之一。它的主要目的是在函式型組件中引入和管理狀態。在以前,狀態只能在類別型組件中使用,但現在有了 useState Hook,我們可以在函式型組件中同樣輕鬆地處理狀態。

所謂的 Hook 其實講直白一點就是一個函式,他的機制其實是在函式的 return 部分回傳所需要內容到一個陣列 [],並讓我們用解構賦值的方法調度,如 useState 的函式就是回他目前的數值與一個函式。

const [state, setState] = useState(value);
// state 代表目前這個狀態的數值
// setState 是 useState 這個函式提供給我們的一個調度用函式
// value 是這個 useState 的預設值

注意:在使用上因為是陣列的解構賦值,因此其實不管用什麼命名方式都可以,但在設計上我們還是會習慣用變數名稱與前面加 set{變數名稱}

Hook 允許我們在函式型組件中添加狀態。我們可以通過它創建和管理組件的狀態變數,並在組件渲染時實現狀態的更新,而需注意的是,每當我們在進行 useState 「設置函式」的調度時,該組件其實都會進行一次更新,這點我們可以在組件中掛載一個 console.log 來得知。

import React, { useState } from 'react';

function MyComponent() {
  // 使用 useState 定義一個狀態變數和一個函數來更新它
  const [count, setCount] = useState(0);
  
  console.log('我在 setCount 被調度時會觸發')
  // count 是狀態變數的名稱,setCount 是更新該狀態的函數
  // useState 函數的參數是狀態的初始值,這裡設置為 0

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

範例均須使用 Server 跑起來,可以使用 Vscode 的 Live Server 套件輔助

基礎 useState 使用

以下先讓我們回憶一下,在 JavaScript 中變數有「傳值」跟「傳址」兩個差別,這可以在前面 JavaScript 的基礎文章中看到,而在 React 中,這兩個差異將會造成比較大的影響,以下就讓我們來看看差別。

純值的使用範例

在使用純值時,setState 的調度時所輸入得函式,會直接變更 state ,因此在使用上非常直覺,但要注意的是 setState 的函式並不是一個同步函式,他其實會在調度後觸發組件更新,並且多個 setState 會同時在該階段執行,因此如果我們要同時調度同一個 set 函式的話,需要額外用一個運算函式來處理,以下有範例可以參考
概念上可以理解覺,因位他們在函式重建時才取得運算值,因此兩個函式取得的基礎運算值會是在相同的記憶體位子,因此不額外用累計的方法無法觸發更新,但在實務上我們基本不會連續觸發 set 函式

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World</title>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <!-- 不要使用他在正式的專案上 -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      const { useState } = React;

      const App = () => {
        const [count, setCount] = useState(0);

        // 因為實際上為了效能,React 會將多次調用的 setCount 合併成一次,因此只有最後一次的 count 會被使用
        const numberCount_1 = () => {
          console.log('我正在執行 numberCount_1');
          console.log(count);
          setCount(count + 1);
          console.log(count);
          setCount(count + 1);
          console.log(count);
        };

        // 但我們可以藉由函式包覆的方式,讓每次調用的 count 都是最新的,而不是被合併,因此可以正確的累加,但效能會較差
        const numberCount_2 = () => {
          console.log('我正在執行 numberCount_2');
          setCount((prevValue) => {
            console.log(prevValue);
            return prevValue + 1;
          });
          setCount((prevValue) => prevValue + 1);
          setCount((prevValue) => prevValue + 1);
        };

        console.log('-----------------');
        console.log('我被重建了');
        console.log('-----------------');

        return (
          <main>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>
              直接調用 set 函式
            </button>
            <button onClick={() => numberCount_1()}>
              使用函式包覆並多次調度
            </button>
            <button onClick={() => numberCount_2()}>
              使用函式包覆並修改操作方式
            </button>
            <button
              onClick={() => {
                setCount(count + 1);
                setCount(count + 1);
                setCount(count + 1);
              }}
            >
              這等於 numberCount_1 的方式
            </button>
          </main>
        );
      };

      ReactDOM.render(<App />, document.getElementById('root'));
    </script>
  </body>
</html>

操作物件與陣列的概念

而在物件與陣列上,setState 就會稍微複雜起來,因為他不是純值,因此他會進行傳「位址」的動作,並且 React 得更新機制會認定這個「位址」而做更新,在下面的範例中第一個陣列我們使用了 push 來增加陣列內的項目,但是畫面上並不會看到更新,因為 React 認為他的位子沒有變更,所以畫面不會重新繪製,但在實際上,push 的內容是有進入該陣列的。
因此我們在操作這部分時,需要進行解構等操作來重新傳入一個新的陣列或物件,才能正常觸發 React 得更新機制。

物件與陣列的基礎操作方式

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World</title>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <!-- 不要使用他在正式的專案上 -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      .container {
        margin: 20px;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      const { useState } = React;

      const ArrayOperateComponent = () => {
        const [numberArray, setNumberArray] = useState([1, 2, 3, 4]);

        // 這樣寫是不行的,因為 React 會認為你沒有改變陣列的內容
        const unableHandleSetNumberArray = () => {
          numberArray.push(5);
          console.log(numberArray);
          setNumberArray(numberArray);
        };

        //
        const setNumberArray_1 = () => {
          setNumberArray([...numberArray, numberArray.length + 1]);
        };

        // 這樣寫是可以的,因為 React 會認為你改變陣列的內容
        const setNumberArray_2 = () => {
          setNumberArray((prevNumberArray) => {
            return [...prevNumberArray, numberArray.length + 1];
          });
        };

        // 這個一樣只能觸發一次
        const setNumberArray_3 = () => {
          setNumberArray([...numberArray, numberArray.length + 1]);
          setNumberArray([...numberArray, numberArray.length + 1]);
          setNumberArray([...numberArray, numberArray.length + 1]);
          setNumberArray([...numberArray, numberArray.length + 1]);
        };

        // 這樣可以觸發多次,但在同一時間的 length 會是一樣的
        const setNumberArray_4 = () => {
          const newArray = JSON.parse(JSON.stringify(numberArray));
          newArray.push(newArray.length + 1);
          setNumberArray(newArray);
        };

        return (
          <article>
            <h1>陣列控制: </h1>
            <nav>
              <button onClick={() => setNumberArray([1, 2, 3, 4, 5])}>
                賦予它新陣列
              </button>
              <button onClick={() => unableHandleSetNumberArray()}>
                用函式處理
              </button>
              <button onClick={() => setNumberArray_1()}>這要調度會成功</button>
              <button onClick={() => setNumberArray_2()}>這也是一種做法</button>
              <button onClick={() => setNumberArray_3()}>這將只調度一次</button>
              <button onClick={() => setNumberArray_4()}>
                這會成功觸發兩次
              </button>
            </nav>
            <p>陣列內容:{numberArray.join('、')}</p>
          </article>
        );
      };

      const ObjectOperateComponent = () => {
        const [personObject, setPersonObject] = useState({
          firstName: 'John',
          lastName: 'Doe',
          age: 18,
          email: 'foo@gmail.com',
        });

        // 這樣寫是不行的,因為 React 會認為你沒有改變物件的內容
        const unableHandleSetPersonObject = () => {
          personObject.firstName = 'iffy';
          console.log(personObject);
          setPersonObject(personObject);
        };

        // 這樣寫是可以的,因為 React 會認為你改變物件的內容
        const setPersonObject_1 = () => {
          setPersonObject({
            ...personObject,
            age: personObject.age + 1,
          });
        };

        // 這樣寫是可以的,因為 React 會認為你改變物件的內容
        const setPersonObject_2 = () => {
          setPersonObject((prevObject) => {
            return {
              ...personObject,
              age: prevObject.age + 1,
            };
          });
        };

        // 這個一樣只能觸發一次
        const setPersonObject_3 = () => {
          setPersonObject({ ...personObject, age: personObject.age + 1 });
          setPersonObject({ ...personObject, age: personObject.age + 1 });
          setPersonObject({ ...personObject, age: personObject.age + 1 });
          setPersonObject({ ...personObject, age: personObject.age + 1 });
        };

        // 這樣可以觸發多次,但在同一時間的 length 會是一樣的
        const setPersonObject_4 = () => {
          setPersonObject((prevObject) => {
            return { ...personObject, age: prevObject.age + 1 };
          });
          setPersonObject((prevObject) => {
            return { ...personObject, age: prevObject.age + 1 };
          });
        };

        return (
          <article>
            <h1>物件控制: </h1>
            <nav>
              <button
                onClick={() =>
                  setPersonObject({
                    firstName: 'Alex',
                    lastName: 'Doe',
                    age: 20,
                    email: 'boo@gmail.com',
                  })
                }
              >
                賦予它新物件
              </button>
              <button onClick={() => unableHandleSetPersonObject()}>
                用函式處理
              </button>
              <button onClick={() => setPersonObject_1()}>
                這要調度會成功
              </button>
              <button onClick={() => setPersonObject_2()}>
                這也是一種做法
              </button>
              <button onClick={() => setPersonObject_3()}>
                這將只調度一次
              </button>
              <button onClick={() => setPersonObject_4()}>
                這會成功觸發兩次
              </button>
            </nav>
            <p>
              姓名:{personObject.firstName} {personObject.lastName}
              <br />
              年紀:{personObject.age}
              <br />
              信箱:{personObject.email}
              <br />
            </p>
          </article>
        );
      };

      const App = () => {
        return (
          <main className='container'>
            <ArrayOperateComponent />
            <br />
            <ObjectOperateComponent />
          </main>
        );
      };

      ReactDOM.render(<App />, document.getElementById('root'));
    </script>
  </body>
</html>

進階的範例

以下的範例我們稍微增加了 UI 的部分,並且使用了陣列與物件的復合應用

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World</title>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

    <!-- 不要使用他在正式的專案上 -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      .container {
        margin: 20px;
      }

      .cart {
        display: flex;
        flex-direction: column;
      }

      .cart > li {
        display: flex;
        margin-bottom: 20px;
        padding: 20px;
        border: solid 1px #cbcbcb;
      }

      .cart__cover {
        margin-right: 20px;
        width: 250px;
        height: 100px;
      }

      .cart__info > * {
        margin-bottom: 10px;
      }

      .cart__info > *:nth-last-child(1) {
        margin-bottom: 0;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      const { useState } = React;

      const cartDate = [
        {
          id: 1,
          name: '超級鉛筆',
          price: 290,
          quantity: 2,
          image: 'https://fakeimg.pl/250x100/',
        },
        {
          id: 2,
          name: '超級橡皮差',
          price: 390,
          quantity: 1,
          image: 'https://fakeimg.pl/250x100/',
        },
      ];

      const App = () => {
        const [cart, setCart] = useState(cartDate);
        const [key, setKey] = useState('');

        const createItem = () => {
          const id = Math.floor(Math.random() * Date.now());
          const name = `Product ${id}`;
          const price = Math.floor(Math.random() * 1000);
          const quantity = Math.floor(Math.random() * 10);
          const image = `https://fakeimg.pl/250x100/?text=${name}`;
          const newItem = {
            id,
            name,
            price,
            quantity,
            image,
          };
          const newCart = [...cart, newItem];
          setCart(newCart);
        };

        const deleteItem = (id) => {
          const newCart = cart.filter((item) => item.id !== id);
          setCart(newCart);
        };

        return (
          <main className='container'>
            <h1>Cart: </h1>
            <input
              type='text'
              value={key}
              onChange={(e) => setKey(e.target.value)}
            />
            <br />
            <nav>
              <button onClick={() => createItem()}>新增產品</button>
            </nav>
            <ul className='cart'>
              {cart
                .filter((item) => item.name.includes(key))
                .map((item) => (
                  <li key={item.id}>
                    <aside className='cart__cover'>
                      <img src={item.image} alt={item.name} />
                    </aside>
                    <article className='cart__info'>
                      <h2>{item.name}</h2>
                      <p>價錢:{item.price}</p>
                      <p>數量:{item.quantity}</p>
                      <p>總計:{item.price * item.quantity}</p>
                      <button onClick={() => deleteItem(item.id)}>
                        刪除產品
                      </button>
                    </article>
                  </li>
                ))}
            </ul>
          </main>
        );
      };

      ReactDOM.render(<App />, document.getElementById('root'));
    </script>
  </body>
</html>

useEffect 學習

useEffect 最主要的功能是用來處理所謂的副作用(side effects)。
副作用通常包括資料請求、訂閱、手動DOM操作、設置定時器等操作,這些操作可能會影響組件的狀態和呈現,並且他們都不是使用者本身所要觸發的事情。

以下是對 useEffect 的詳細介紹:
使用 useEffect Hook 非常簡單。它接受兩個參數:一個是回乎函式,用於執行副作用操作,另一個是依賴項目(可選的)。

而 return 是指當這個回呼函式要重新執行或消散(該組件註銷或移除)時,要觸發的函式。

  useEffect(() => {
    first
  
    return () => {
      second
    }
  }, [third])
// first 在這裡執行副作用操作
// second 再者裡執行當這個組件重建或是參考值更新時的行為
// third 依賴項目

useEffect 在組件建構時會被執行一次,並根據他依賴項目的內容,決定是否再次執行,而再次執行時,useEffect 中的 return 都會先在觸發一次

在 React 18 版之後,useEffect 在開發時會自己先跑一次運作邏輯,這是他在保證你的 useEffect 整體能運作正常,而在生產環境(編譯出來的版本),就不會有這個問題了

副作用(Side Effect)是什麼?

副作用是指與組件渲染以外的操作,這些操作可能會改變應用程序的狀態或影響UI呈現。舉例來說,當您需要發送網路請求以獲取資料,訂閱等外部事件,或者進行手動DOM操作時,都屬於副作用操作。

說的白話文一點,就是當我們去看醫生時會掛號,而目前看病的號碼跟我們手中的號碼相同時,我們才可以進去診間讓醫生看診,而「每一次號碼切換時」我們都會做一次確認的動作,而這就是屬於一種 Side Effect」

  useEffect(() => {
    進入病房
  
    return () => {
      繼續等待或者是等太久決定回家的邏輯
    }
  }, [看病號碼])

簡單的範例

這是一個這是一個常見的累計 effect,每當 count 的數值變化時,會觸發 useEffect 的運作,因為他依賴了 count 這個數值,並且裡面會執行一個一秒的倒計時,在一秒後會呼叫 set 函式並且 +1,而當這個計時器運作時會觸發 setCount 從而推動 count 變動,並且先執行 return 的函式清除這個數值計時器,並在進行新的循環。

  • 初次建立組件時計時器建立
  • 計時器運作觸發 set 函式
  • count 觸發連動 return 執行
  • return 執行完成後重新執行 useEffect 中的回呼函式
  • 第二次建立計時器
  • ...
import React, { useState, useEffect } from 'react';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 在每次渲染後設置一個計時器
    const timer = setInterval(() => {
      setCount(count + 1);
    }, 1000);

    // 清理副作用:組件結束時停止計時器
    return () => clearInterval(timer);
  }, [count]);

  return (
    <div>
      <p>計時器: {count}</p>
    </div>
  );
}

上一篇
React 基礎概念學習與認知
下一篇
了解 React 中的資料流與相關 Hook 操作
系列文
給前端新手的圖文故事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言